/*
 * File     : SizedStructureField.java
 * Created  : 12 Nov 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.structure;

import static com.googlecode.dni.internal.DniTypes.STRUCTURE_HELPER_TYPE;
import static org.objectweb.asm.Opcodes.ALOAD;

import java.lang.reflect.Method;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.googlecode.dni.DynamicSize;
import com.googlecode.dni.FixedSize;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.type.structure.IllegalStructureException;

/**
 * A {@link StructureField} that has a fixed or dynamic size (optionally).
 *
 * @author Matthew Wilson
 */
abstract class SizedStructureField extends StructureField
{

    private FixedSize fixedSize;

    private DynamicSize dynamicSize;

    /**
     * {@inheritDoc}
     */
    @Override
    void setGetter( final Method getter )
    {
        super.setGetter( getter );

        this.fixedSize = getter.getAnnotation( FixedSize.class );
        this.dynamicSize = getter.getAnnotation( DynamicSize.class );

        if ( this.fixedSize != null && this.dynamicSize != null )
        {
            throw new IllegalStructureException( "Cannot be @FixedSize and @DynamicSize: " + getter );
        }
        if ( this.fixedSize != null && this.fixedSize.value() < 0 )
        {
            throw new IllegalStructureException( "@FixedSize( " + this.fixedSize.value() + " ) is negative: " + getter );
        }
    }

    /** @return <code>true</code> iff sized */
    final boolean isSized()
    {
        return this.fixedSize != null || this.dynamicSize != null;
    }

    /** @return <code>true</code> iff fixed-sized */
    final boolean isFixedSized()
    {
        return this.fixedSize != null;
    }

    /** @return <code>true</code> iff dynamically sized */
    final boolean isDynamicallySized()
    {
        return this.dynamicSize != null;
    }

    /**
     * Writes code to put the size onto the stack (as an <code>int</code>).
     *
     * @param methodVisitor
     *            the method visitor
     * @param implName
     *            the implementation name
     * @param multiplier
     *            a multiplier that the value will be multiplied by
     */
    final void writeGetSize( final MethodVisitor methodVisitor,
                             final String implName,
                             final int multiplier )
    {
        if ( this.fixedSize != null )
        {
            int size = this.fixedSize.value();
            if ( size * (long) multiplier > Integer.MAX_VALUE )
            {
                throw new IllegalStructureException( "FixedSize(" + size + " is too big for unit size of " + multiplier );
            }

            DniInternal.writeConstantInt( methodVisitor, size * multiplier );
            return;
        }

        String sizeFieldName = this.dynamicSize.value();

        StructureField sizeField = getFieldMap().getField( sizeFieldName );
        if ( sizeField == null )
        {
            throw new IllegalStructureException( "Missing size field '" + sizeFieldName + "' for '" + getName() + "'" );
        }
        if ( !( sizeField instanceof PrimitiveStructureField ) )
        {
            throw new IllegalStructureException( "Invalid size field '" + sizeFieldName + "' for '" + getName() + "'" );
        }

        PrimitiveStructureField primitiveSizeField = (PrimitiveStructureField) sizeField;
        primitiveSizeField.getGetter();

        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                       implName,
                                       "get" + sizeFieldName,
                                       Type.getMethodDescriptor( sizeField.getGetter() ) );

        Class< ? > sizeType = sizeField.getGetter().getReturnType();
        if ( sizeType == byte.class
             || sizeType == short.class
             || sizeType == char.class
             || sizeType == int.class )
        {
            // these types are fixed sizes and obey normal Java signed/unsigned rules

            String methodSig = "(I)I";
            if ( multiplier != 1 )
            {
                // check for overflow when dealing with ints
                assert Integer.bitCount( multiplier ) == 1 : multiplier;

                int shift = 31 - Integer.numberOfLeadingZeros( multiplier );
                DniInternal.writeConstantInt( methodVisitor, shift );

                assert 1 << shift == multiplier : multiplier;

                methodSig = "(II)I";
            }

            methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                           STRUCTURE_HELPER_TYPE,
                                           "checkIntAsSize",
                                           methodSig );
        }
        else if ( sizeType == long.class )
        {
            // convert the long value to an int; ensure that 'big' values cause
            // a failure when appropriate

            String methodSig = "(J)I";
            if ( multiplier != 1 )
            {
                // use an overload that takes the unit size and avoids
                // overflowing maths with longs
                DniInternal.writeConstantInt( methodVisitor, multiplier );
                methodSig = "(JI)I";
            }

            methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                           STRUCTURE_HELPER_TYPE,
                                           "checkLongAsSize",
                                           methodSig );
        }
        else
        {
            throw new IllegalStructureException( "Invalid type of size field '" + sizeFieldName + "' for '" + getName() + "'" );
        }
    }


}
